﻿using System;
using System.Collections.Generic;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Fintech.Utils;
using ZOVNP.Model.Abstracts;

namespace ZOVNP.Engine
{
    /// <summary> Класс, реализующий потокобезопасный доступ к навигационным свойствам типа "ссылка" доменных объектов. </summary>
    /// <typeparam name="OwnerType"> Тип объекта-владельца данной ссылки (исходящий конец ссылки). </typeparam>
    /// <typeparam name="ImplType"> Тип реализации доменного объекта. </typeparam>
    /// <typeparam name="InterfaceType"> Тип интерфейса доменного объекта. </typeparam>
    /// <remarks> Поддерживается обработка ошибок с восстановлением соединения с хранилищем (если оно было нарушено). </remarks>
    internal sealed class SmartReference<OwnerType, ImplType, InterfaceType> : SmartRelation, IDomainReference<InterfaceType>
        where OwnerType : DomainObject
        where ImplType : DomainObject, InterfaceType
        where InterfaceType : class, IDomainObject
    {
        #region Поля

        /// <summary> Хеш выражения, задающего делегат получения ключа. </summary>
        private int expressionHash;

        #endregion

        #region Конструкторы

        /// <summary> Конструктор с навигационным свойством. </summary>
        /// <param name="Owner"> Владелец навигационного свойства (исходящего конца навигационного свойства). </param>
        /// <param name="Reference"> Навигационное свойство "ссылка". </param>
        /// <param name="IDSelector"> Выражение, задающее ключ объекта, на который ведёт ссылка. </param>
        public SmartReference(OwnerType Owner, EntityReference<ImplType> Reference, Expression<Func<OwnerType, Guid?>> IDSelector)
            : base(Owner, Reference)
        {
            IDSelector.CheckOnNull("IDSelector");
            PutReference(IDSelector, expressionHash = IDSelector.ToString().GetHashCode());
        }

        #endregion

        #region Свойства

        internal EntityReference<ImplType> InnerReference { get { return (EntityReference<ImplType>)relation; } }

        /// <summary> Получить идентификатор-ключ доменного объекта, на который стоит навигационное свойство. </summary>
        /// <remarks> Этот идентификатор иногда доступен даже без загрузки связанного с ним доменного объекта. </remarks>
        public System.Guid? ID
        {
            get
            {
                var refDelegate = (Func<OwnerType, Guid?>)GetReference().GetCompiledExpression();
                return refDelegate((OwnerType)Owner);
            }
        }

        /// <summary> Получить значение навигационного свойства. </summary>
        /// <remarks> 
        /// Значение навигационного свойства зависит от типа навигации (1 к многим, многие к 1, 1 к 1 и тд)
        /// и может возвращать либо ссылку на единичный объект, либо ссылку на коллекцию объектов. В случае
        /// если возвращается ссылка на объект, то она может иметь значение null. Если же возвращается ссылка
        /// на коллекцию объектов, и эта коллекция пуста, то свойство содержит экземпляр пустой коллекции. 
        /// </remarks>
        public override object Navigation { get { return Value; } }

        /// <summary> Получить/установить значение навигационного свойства. </summary>
        public InterfaceType Value
        {
            get
            {
                TryLoadNavigation(true);
                return (relation as EntityReference<ImplType>).Value;
            }
            set
            {
                var casted = value as ImplType;
                if (value != null && casted == null)
                    throw new System.ArgumentException("Значение свойства должно иметь тип " + typeof(ImplType));
                TryLoadNavigation(true);
                (relation as EntityReference<ImplType>).Value = casted;
            }
        }

        #endregion

        #region Методы

        /// <summary> Получение метаданных о текущей ссылке. </summary>
        /// <returns> Описатель выражения, которое нужно вычислить для получения значения ссылки. </returns>
        private ReferenceEntry GetReference()
        {
            return references[typeof(OwnerType)].Find(it => it.DestinationType == typeof(ImplType) && it.ExpressionHash == expressionHash);
        }

        /// <summary> Регистрация ссылочного выражения в кеше метаданных. </summary>
        /// <param name="IDSelector"> Выражение. </param>
        /// <param name="ExpressionHash"> Хеш ссылочного выражения. Нужен для случая, когда одна сущность имеет несколько ссылок на другую сущность, но на разные её свойства. </param>
        private static void PutReference(Expression<Func<OwnerType, Guid?>> IDSelector, int ExpressionHash)
        {
            var type = typeof(OwnerType);
            var dest = typeof(ImplType);
            List<ReferenceEntry> entries;
            lock (referencesLock)
            {
                if (!references.TryGetValue(type, out entries))
                {
                    references.Add(type, entries = new List<ReferenceEntry>());
                    entries.Add(new ReferenceEntry(dest, IDSelector, ExpressionHash));
                }
                else
                {
                    if (entries.Find(it => it.DestinationType == dest && it.ExpressionHash == ExpressionHash) == null)
                        entries.Add(new ReferenceEntry(dest, IDSelector, ExpressionHash));
                }
            }
        }

        /// <summary> Обработчик для окончания загрузки навигационного свойства. </summary>
        protected override void OnRelationLoaded()
        {
            base.OnRelationLoaded();
        }

        /// <summary> Преобразовать к строке. </summary>
        /// <returns> Строковое представление объекта. </returns>
        public override string ToString()
        {
            if (IsLoaded)
                return string.Format("Ссылка на {0} с {1}; загружена, значение = {2}", typeof(ImplType).Name, typeof(OwnerType).Name, Value != null ? Value.ToString() : "null");
            else
                return string.Format("Ссылка на {0} с {1}; не загружена.", typeof(ImplType).Name, typeof(OwnerType).Name);
        }

        #endregion
    }
}
